﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using FBGraph.ClientGenerator.CodeGen;
using FBGraph.ClientGenerator.Documentation;

namespace FBGraph.ClientGenerator
{
    internal class Generator
    {
        private static readonly String[] CommonPropertyTypes = new String[] { "from", "to", "comments", "owner", "location", "venue" };
        
        public Generator()
        {
            this.Types = new List<CodeGenType>();
            this.Documentation = TypeDocumentation.Load("Documentation.xml");
        }

        private List<CodeGenType> Types { get; set; }
        private TypeDocumentation Documentation { get; set; }

        internal CodeGenType[] Generate(ObjectData[] objectData)
        {
            // process user first so that walking the connections fills out the other classes
            objectData.OrderByDescending(o => o.Name == "User").ToList().ForEach(obj => GenerateClass(obj));
            this.FillDocumentation();
            return this.Types.ToArray();
        }

        private void FillDocumentation()
        {
            foreach (var type in this.Types)
            {
                FillDocumentation(null, type);
            }
        }

        private void FillDocumentation(String fullParentTypeName, CodeGenType type)
        {
            var fullTypeName = String.Format("{0}.{1}", fullParentTypeName ?? type.Namespace, type.Name);
            if (this.Documentation.HasDocumentationFor(fullTypeName))
            {
                foreach (var doc in this.Documentation.GetDocumentation(fullTypeName))
                {
                    type.XmlDocComments.Suggest(doc);
                }
            }

            foreach (var prop in type.Properties)
            {
                var fullPropertyName = String.Format("{0}.{1}", fullTypeName, prop.Name);
                if (this.Documentation.HasDocumentationFor(fullPropertyName))
                {
                    foreach (var doc in this.Documentation.GetDocumentation(fullPropertyName))
                    {
                        prop.XmlDocComments.Suggest(doc);
                    }
                }
            }

            foreach (var nestedType in type.NestedTypes)
            {
                FillDocumentation(fullTypeName, nestedType);
            }
        }

        /// <summary>Initializes a new <see cref="GraphTypeDeclaration" /> or returns an existing type for updating/</summary>
        /// <remarks>Due to the grossly incomplete state of Facebook's Graph API documentation, several different passes are attempted
        /// to get complete information about each type. Subsequent attempts may reveal better information which may need to replace
        /// information that was already gathered.</remarks>
        private CodeGenType GetTypeDeclaration(String typeName, String namespaceName, CodeGenType parentType)
        {
            if (parentType == null)
            {
                var existingType = this.Types.SingleOrDefault(t => t.Name == typeName);
                if (existingType != null) return existingType;
                else
                {
                    var typeDec = new CodeGenType(typeName, namespaceName);
                    this.Types.Add(typeDec);
                    return typeDec;
                }
            }
            else return parentType.GetNestedType(typeName);
        }
        private CodeGenType GetTypeDeclaration(String typeName, String namespaceName) { return this.GetTypeDeclaration(typeName, namespaceName, null); }

        /// <summary>Generates a class declaration</summary>
        private void GenerateClass(ObjectData obj)
        {
            var type = this.GetTypeDeclaration(obj.Name.ToClassName(), "FBGraph.Models");
            type.Imports.Suggest("Newtonsoft.Json");

            type.XmlDocComments.Suggest("<summary>{0}</summary>", obj.Description);

            type.XmlDocComments.Suggest("<remarks>");
            foreach (var remark in obj.Remarks)
            {
                type.XmlDocComments.Suggest("<para>{0}</para>", remark);
            }
            type.XmlDocComments.Suggest("<para>{0}</para>", String.Format("Example: {0} ({1})", obj.ExampleUrl, obj.ExampleDescription));
            type.XmlDocComments.Suggest("</remarks>");

            foreach (var propertyData in obj.Properties)
            {
                GenerateFieldAndProperty(propertyData.ExampleData, propertyData.ExampleData, propertyData.Name, propertyData.Description, type);
            }

            foreach (var connectionData in obj.Connections)
            {
                if (connectionData.Name != "picture")
                {
                    foreach (var detailExample in connectionData.ExampleDetailData)
                    {
                        GenerateClassFromJson(detailExample, connectionData.Name, null);
                    }

                    var connectionType = GenerateClassFromJson(connectionData.ExampleData, connectionData.Name, null);
                    GenerateConnectionController(type, connectionData, connectionType);
                }
            }

            type.InterfaceImplementations.Suggest(typeof(IFBGraphResult));
        }

        private void GenerateConnectionController(CodeGenType containingType, ObjectData.ConnectionData connectionData, TypeReference connectionType)
        {
            var targetNamespace = String.Format("FBGraph.Controllers.Connections.{0}s", containingType.Name);

            var type = GetTypeDeclaration(connectionData.Name.ToPascalCase() + "Controller", targetNamespace);
            // if the type's namespace isn't set to the supplied target namespace, it's because it already exists.
            // this implies that the connection is shared between multiple objects, and should remain in the root FBGraph.Controllers namespace.
            if (type.Namespace != targetNamespace) type.Namespace = "FBGraph.Controllers";            

            type.InheritFrom(new TypeReference(typeof(FBGraphController)));
            var connectionTypeRef = connectionType.TypeArguments.FirstOrDefault();
            if (connectionTypeRef != null)
            {
                var controllerBaseType = new TypeReference(typeof(IFBGraphConnectionController<>));
                if (connectionTypeRef != null)
                {
                    controllerBaseType.TypeArguments.Add(connectionTypeRef);
                }
                type.InterfaceImplementations.Suggest(controllerBaseType);                

                var connectionName = new Property("ConnectionName", typeof(String))
                {
                    ExplicitImplementationType = new TypeReference(typeof(IFBGraphConnectionController<>), connectionTypeRef),
                    SetAccessibility = "private",
                    HasGet = true,
                    GetBody = String.Format("return \"{0}\";", connectionData.Name)
                };
                connectionName.XmlDocComments.Suggest("<summary>Gets the name of the connection the <see cref=\"{0}\" /> handles requests for.</summary>", type.Name);
                type.Properties.Suggest(connectionName);
            }

            type.Imports.Suggest("FBGraph.Models");

            type.XmlDocComments.Suggest("<summary>{0}</summary>", connectionData.Description);

            var ctr = new Constructor(type, new Parameter("context", typeof(IFBGraphContext)));
            ctr.XmlDocComments.Suggest("<summary>Initializes an instance of <see cref=\"{0}\" />.</summary>", type.Name);
            ctr.ChainedConstructor = new ChainedConstructor("base", "context");
            type.Constructors.Suggest(ctr);
        }

        private void GenerateFieldAndProperty(ExampleData source, ExampleData exampleData, String propertyName, String propertyDescription, CodeGenType type)
        {
            var propertyType = DeterminePropertyType(exampleData, propertyName, type);

            if (propertyType != null)
            {
                var property = new AutoProperty(propertyName.ToPascalCase(), propertyType);
                property.XmlDocComments.Suggest("<summary>{0}</summary>", propertyDescription);
                property.CustomAttributes.Suggest(typeof(JsonPropertyAttribute), new Argument(propertyName));

                /*if (source != null && source.GetType() != typeof(JValue) && source.GetType() != typeof(JArray) && source["id"] != null)
                {
                    property.XmlDocComments.Suggest("<source>https://graph.facebook.com/{0}?access_token={1}</source>", ((JValue)source["id"]).Value, SavedAccessToken.Default.GeneratorToken);
                }*/

                type.Properties.Suggest(property);
            }
        }

        private TypeReference DeterminePropertyType(ExampleData exampleData, String propertyName, CodeGenType parentType)
        {
            if (exampleData.Data is JValue)
            {
                var value = ((JValue)exampleData.Data).Value;
                String strValue = value == null ? null : value.ToString();
                Boolean boolValue;
                Decimal decValue;
                Int64 longValue;
                DateTime dtValue;

                if (String.IsNullOrEmpty(strValue)) return new TypeReference(typeof(Object)); // indicates that a null or empty value was encountered, but the type is not known
                else if (Boolean.TryParse(strValue, out boolValue)) return new TypeReference(typeof(Boolean));
                else if (strValue.Contains(".") && Decimal.TryParse(strValue, out decValue)) return new TypeReference(typeof(Decimal));
                else if (Int64.TryParse(strValue, out longValue)) return new TypeReference(typeof(Int64));
                else if (DateTime.TryParse(strValue, out dtValue)) return new TypeReference(typeof(DateTime));
                else return new TypeReference(typeof(String));
            }
            else
            {
                var childType = GenerateClassFromJson(exampleData, propertyName, parentType);
                return childType;
            }
        }

        private TypeReference GenerateClassFromJson(ExampleData json, String propertyName, CodeGenType parentType)
        {
            if (json.Data == null) return new TypeReference(typeof(Object));

            var typeName = String.Format("{0}{1}",
                    parentType == null ? String.Empty : parentType.Name.ToPascalCase(),
                    propertyName.ToPascalCase().Depluralize());

            var isCommonPropertyType = CommonPropertyTypes.Contains(propertyName);
            if (isCommonPropertyType)
            {
                typeName = propertyName.ToPascalCase().Depluralize();
            }

            if (json.Data.Type == JTokenType.Array)
            {
                TypeReference type = null;

                foreach (var item in json.Data)
                {
                    type = DeterminePropertyType(new ExampleData(item, json.SourceUri), propertyName, parentType);
                }
                if (type.Name == typeName) return new TypeReference(String.Format("{0}[]", typeName));
                else return type;
            }
            else if (json.Data.First.Type == JTokenType.Array)
            {
                TypeReference type = null;
                foreach (var item in json.Data.First)
                {
                    type = DeterminePropertyType(new ExampleData(item, json.SourceUri), propertyName, parentType);
                }
                if (type.Name == typeName) return new TypeReference(String.Format("{0}[]", typeName));
                else return type;
            }
            else if (json.Data.First is JProperty && ((JProperty)json.Data.First).Name == "data" && ((JProperty)json.Data.First).First.Type == JTokenType.Array)
            {
                TypeReference type = null;
                foreach (var item in ((JProperty)json.Data.First).First)
                {
                    type = DeterminePropertyType(new ExampleData(item, json.SourceUri), propertyName, parentType);
                }
                var pagedType = new TypeReference(typeof(PagedResult<>));
                if(type == null) pagedType.TypeArguments.Add(new TypeReference(typeof(String)));
                else pagedType.TypeArguments.Add(type);

                return pagedType;
            }
            else
            {
                CodeGenType type = null;
                String namespaceName = "FBGraph.Models";

                type = GetTypeDeclaration(typeName, namespaceName, isCommonPropertyType ? null : parentType);
                type.Imports.Suggest("Newtonsoft.Json");

                foreach (var property in json.Data.Cast<JProperty>())
                {
                    GenerateFieldAndProperty(json, new ExampleData(property.Value, json.SourceUri), property.Name, null, type);
                }

                if (parentType == null)
                {
                    type.InterfaceImplementations.Suggest(typeof(IFBGraphResult));
                }

                return new TypeReference(type);
            }
        }
    }
}